﻿// ------------------------------------------------------------------------
// Copyright 2023 The Dapr Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//     http://www.apache.org/licenses/LICENSE-2.0
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// ------------------------------------------------------------------------

namespace Dapr.Actors.Generators;

using System.Text;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Testing;
using Microsoft.CodeAnalysis.Text;
using VerifyCS = CSharpSourceGeneratorVerifier<ActorClientGenerator>;

public sealed class ActorClientGeneratorTests
{
    private const string ActorMethodAttributeText = $@"// <auto-generated/>
#nullable enable
using System;

namespace Dapr.Actors.Generators
{{
    [AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = false)]
    internal sealed class ActorMethodAttribute : Attribute
    {{
        public string? Name {{ get; set; }}
    }}
}}";

    private static readonly (string, SourceText) ActorMethodAttributeSource = (
        Path.Combine("Dapr.Actors.Generators", "Dapr.Actors.Generators.ActorClientGenerator", "Dapr.Actors.Generators.ActorMethodAttribute.g.cs"),
        SourceText.From(ActorMethodAttributeText, Encoding.UTF8));

    private const string GenerateActorClientAttributeText = $@"// <auto-generated/>
#nullable enable
using System;

namespace Dapr.Actors.Generators
{{
    [AttributeUsage(AttributeTargets.Interface, AllowMultiple = false, Inherited = false)]
    internal sealed class GenerateActorClientAttribute : Attribute
    {{
        public string? Name {{ get; set; }}
        public string? Namespace {{ get; set; }}
    }}
}}";

    private static readonly (string, SourceText) GenerateActorClientAttributeSource = (
        Path.Combine("Dapr.Actors.Generators", "Dapr.Actors.Generators.ActorClientGenerator", "Dapr.Actors.Generators.GenerateActorClientAttribute.g.cs"),
        SourceText.From(GenerateActorClientAttributeText, Encoding.UTF8));

    private static VerifyCS.Test CreateTest(string originalSource, string? generatedName = null, string? generatedSource = null)
    {
        var test = new VerifyCS.Test
        {
            TestState =
            {
                AdditionalReferences = { AdditionalMetadataReferences.Actors },
                Sources = { originalSource },
                GeneratedSources =
                {
                    ActorMethodAttributeSource,
                    GenerateActorClientAttributeSource
                },
            }
        };

        if (generatedName is not null && generatedSource is not null)
        {
            test.TestState.GeneratedSources.Add((
                Path.Combine("Dapr.Actors.Generators", "Dapr.Actors.Generators.ActorClientGenerator", generatedName),
                SourceText.From(generatedSource, Encoding.UTF8)));
        }

        return test;
    }

    [Fact]
    public async Task TestMethodWithNoArgumentsOrReturnValue()
    {
        var originalSource = @"
using Dapr.Actors.Generators;
using System.Threading.Tasks;

namespace Test
{
    [GenerateActorClient]
    public interface ITestActor
    {
        Task TestMethod();
    }
}";

        var generatedSource = @"// <auto-generated/>
#nullable enable
namespace Test
{
    public sealed class TestActorClient : Test.ITestActor
    {
        private readonly Dapr.Actors.Client.ActorProxy actorProxy;
        public TestActorClient(Dapr.Actors.Client.ActorProxy actorProxy)
        {
            if (actorProxy is null)
            {
                throw new System.ArgumentNullException(nameof(actorProxy));
            }

            this.actorProxy = actorProxy;
        }

        public System.Threading.Tasks.Task TestMethod()
        {
            return this.actorProxy.InvokeMethodAsync(""TestMethod"");
        }
    }
}";

        await CreateTest(originalSource, "Test.TestActorClient.g.cs", generatedSource).RunAsync();
    }

    [Fact]
    public async Task TestInternalInterface()
    {
        var originalSource = @"
using Dapr.Actors.Generators;
using System.Threading.Tasks;

namespace Test
{
    [GenerateActorClient]
    internal interface ITestActor
    {
        Task TestMethod();
    }
}";

        var generatedSource = @"// <auto-generated/>
#nullable enable
namespace Test
{
    internal sealed class TestActorClient : Test.ITestActor
    {
        private readonly Dapr.Actors.Client.ActorProxy actorProxy;
        public TestActorClient(Dapr.Actors.Client.ActorProxy actorProxy)
        {
            if (actorProxy is null)
            {
                throw new System.ArgumentNullException(nameof(actorProxy));
            }

            this.actorProxy = actorProxy;
        }

        public System.Threading.Tasks.Task TestMethod()
        {
            return this.actorProxy.InvokeMethodAsync(""TestMethod"");
        }
    }
}";

        await CreateTest(originalSource, "Test.TestActorClient.g.cs", generatedSource).RunAsync();
    }

    [Fact]
    public async Task TestSingleGenericInternalInterface()
    {
        var originalSource = @"
using Dapr.Actors.Generators;
using System.Threading.Tasks;

namespace Test
{
    [GenerateActorClient]
    internal interface ITestActor<TGenericType>
    {
        Task TestMethod();
    }
}";

        var generatedSource = @"// <auto-generated/>
#nullable enable
namespace Test
{
    internal sealed class TestActorClient<TGenericType> : Test.ITestActor<TGenericType>
    {
        private readonly Dapr.Actors.Client.ActorProxy actorProxy;
        public TestActorClient(Dapr.Actors.Client.ActorProxy actorProxy)
        {
            if (actorProxy is null)
            {
                throw new System.ArgumentNullException(nameof(actorProxy));
            }

            this.actorProxy = actorProxy;
        }

        public System.Threading.Tasks.Task TestMethod()
        {
            return this.actorProxy.InvokeMethodAsync(""TestMethod"");
        }
    }
}";

        await CreateTest(originalSource, "Test.TestActorClient.g.cs", generatedSource).RunAsync();
    }

    [Fact]
    public async Task TestMultipleGenericsInternalInterface()
    {
        var originalSource = @"
using Dapr.Actors.Generators;
using System.Threading.Tasks;

namespace Test
{
    [GenerateActorClient]
    internal interface ITestActor<TGenericType1, TGenericType2>
    {
        Task TestMethod();
    }
}";

        var generatedSource = @"// <auto-generated/>
#nullable enable
namespace Test
{
    internal sealed class TestActorClient<TGenericType1, TGenericType2> : Test.ITestActor<TGenericType1, TGenericType2>
    {
        private readonly Dapr.Actors.Client.ActorProxy actorProxy;
        public TestActorClient(Dapr.Actors.Client.ActorProxy actorProxy)
        {
            if (actorProxy is null)
            {
                throw new System.ArgumentNullException(nameof(actorProxy));
            }

            this.actorProxy = actorProxy;
        }

        public System.Threading.Tasks.Task TestMethod()
        {
            return this.actorProxy.InvokeMethodAsync(""TestMethod"");
        }
    }
}";

        await CreateTest(originalSource, "Test.TestActorClient.g.cs", generatedSource).RunAsync();
    }

    [Fact]
    public async Task TestRenamedClient()
    {
        var originalSource = @"
using Dapr.Actors.Generators;
using System.Threading.Tasks;

namespace Test
{
    [GenerateActorClient(Name = ""MyTestActorClient"")]
    internal interface ITestActor
    {
        Task TestMethod();
    }
}";

        var generatedSource = @"// <auto-generated/>
#nullable enable
namespace Test
{
    internal sealed class MyTestActorClient : Test.ITestActor
    {
        private readonly Dapr.Actors.Client.ActorProxy actorProxy;
        public MyTestActorClient(Dapr.Actors.Client.ActorProxy actorProxy)
        {
            if (actorProxy is null)
            {
                throw new System.ArgumentNullException(nameof(actorProxy));
            }

            this.actorProxy = actorProxy;
        }

        public System.Threading.Tasks.Task TestMethod()
        {
            return this.actorProxy.InvokeMethodAsync(""TestMethod"");
        }
    }
}";

        await CreateTest(originalSource, "Test.MyTestActorClient.g.cs", generatedSource).RunAsync();
    }

    [Fact]
    public async Task TestSingleGenericRenamedClient()
    {
        var originalSource = @"
using Dapr.Actors.Generators;
using System.Threading.Tasks;

namespace Test
{
    [GenerateActorClient(Name = ""MyTestActorClient"")]
    internal interface ITestActor<TGenericType>
    {
        Task TestMethod();
    }
}";

        var generatedSource = @"// <auto-generated/>
#nullable enable
namespace Test
{
    internal sealed class MyTestActorClient<TGenericType> : Test.ITestActor<TGenericType>
    {
        private readonly Dapr.Actors.Client.ActorProxy actorProxy;
        public MyTestActorClient(Dapr.Actors.Client.ActorProxy actorProxy)
        {
            if (actorProxy is null)
            {
                throw new System.ArgumentNullException(nameof(actorProxy));
            }

            this.actorProxy = actorProxy;
        }

        public System.Threading.Tasks.Task TestMethod()
        {
            return this.actorProxy.InvokeMethodAsync(""TestMethod"");
        }
    }
}";

        await CreateTest(originalSource, "Test.MyTestActorClient.g.cs", generatedSource).RunAsync();
    }

    [Fact]
    public async Task TestMultipleGenericsRenamedClient()
    {
        var originalSource = @"
using Dapr.Actors.Generators;
using System.Threading.Tasks;

namespace Test
{
    [GenerateActorClient(Name = ""MyTestActorClient"")]
    internal interface ITestActor<TGenericType1, TGenericType2>
    {
        Task TestMethod();
    }
}";

        var generatedSource = @"// <auto-generated/>
#nullable enable
namespace Test
{
    internal sealed class MyTestActorClient<TGenericType1, TGenericType2> : Test.ITestActor<TGenericType1, TGenericType2>
    {
        private readonly Dapr.Actors.Client.ActorProxy actorProxy;
        public MyTestActorClient(Dapr.Actors.Client.ActorProxy actorProxy)
        {
            if (actorProxy is null)
            {
                throw new System.ArgumentNullException(nameof(actorProxy));
            }

            this.actorProxy = actorProxy;
        }

        public System.Threading.Tasks.Task TestMethod()
        {
            return this.actorProxy.InvokeMethodAsync(""TestMethod"");
        }
    }
}";

        await CreateTest(originalSource, "Test.MyTestActorClient.g.cs", generatedSource).RunAsync();
    }

    [Fact]
    public async Task TestCustomNamespace()
    {
        var originalSource = @"
using Dapr.Actors.Generators;
using System.Threading.Tasks;

namespace Test
{
    [GenerateActorClient(Namespace = ""MyTest"")]
    internal interface ITestActor
    {
        Task TestMethod();
    }
}";

        var generatedSource = @"// <auto-generated/>
#nullable enable
namespace MyTest
{
    internal sealed class TestActorClient : Test.ITestActor
    {
        private readonly Dapr.Actors.Client.ActorProxy actorProxy;
        public TestActorClient(Dapr.Actors.Client.ActorProxy actorProxy)
        {
            if (actorProxy is null)
            {
                throw new System.ArgumentNullException(nameof(actorProxy));
            }

            this.actorProxy = actorProxy;
        }

        public System.Threading.Tasks.Task TestMethod()
        {
            return this.actorProxy.InvokeMethodAsync(""TestMethod"");
        }
    }
}";

        await CreateTest(originalSource, "MyTest.TestActorClient.g.cs", generatedSource).RunAsync();
    }

    [Fact]
    public async Task TestRenamedMethod()
    {
        var originalSource = @"
using Dapr.Actors.Generators;
using System.Threading.Tasks;

namespace Test
{
    [GenerateActorClient]
    public interface ITestActor
    {
        [ActorMethod(Name = ""MyTestMethod"")]
        Task TestMethod();
    }
}
";

        var generatedSource = @"// <auto-generated/>
#nullable enable
namespace Test
{
    public sealed class TestActorClient : Test.ITestActor
    {
        private readonly Dapr.Actors.Client.ActorProxy actorProxy;
        public TestActorClient(Dapr.Actors.Client.ActorProxy actorProxy)
        {
            if (actorProxy is null)
            {
                throw new System.ArgumentNullException(nameof(actorProxy));
            }

            this.actorProxy = actorProxy;
        }

        public System.Threading.Tasks.Task TestMethod()
        {
            return this.actorProxy.InvokeMethodAsync(""MyTestMethod"");
        }
    }
}";

        await CreateTest(originalSource, "Test.TestActorClient.g.cs", generatedSource).RunAsync();
    }

    [Fact]
    public async Task TestMethodWithArgumentsButNoReturnValue()
    {
        var originalSource = @"
using Dapr.Actors.Generators;
using System.Threading.Tasks;

namespace Test
{
    public record TestValue(int Value);

    [GenerateActorClient]
    public interface ITestActor
    {
        Task TestMethod(TestValue value);
    }
}
";

        var generatedSource = @"// <auto-generated/>
#nullable enable
namespace Test
{
    public sealed class TestActorClient : Test.ITestActor
    {
        private readonly Dapr.Actors.Client.ActorProxy actorProxy;
        public TestActorClient(Dapr.Actors.Client.ActorProxy actorProxy)
        {
            if (actorProxy is null)
            {
                throw new System.ArgumentNullException(nameof(actorProxy));
            }

            this.actorProxy = actorProxy;
        }

        public System.Threading.Tasks.Task TestMethod(Test.TestValue value)
        {
            return this.actorProxy.InvokeMethodAsync<Test.TestValue>(""TestMethod"", value);
        }
    }
}";

        await CreateTest(originalSource, "Test.TestActorClient.g.cs", generatedSource).RunAsync();
    }

    [Fact]
    public async Task TestMethodWithNoArgumentsButReturnValue()
    {
        var originalSource = @"
using Dapr.Actors.Generators;
using System.Threading.Tasks;

namespace Test
{
    public record TestValue(int Value);

    [GenerateActorClient]
    public interface ITestActor
    {
        Task<TestValue> TestMethodAsync();
    }
}";

        var generatedSource = @"// <auto-generated/>
#nullable enable
namespace Test
{
    public sealed class TestActorClient : Test.ITestActor
    {
        private readonly Dapr.Actors.Client.ActorProxy actorProxy;
        public TestActorClient(Dapr.Actors.Client.ActorProxy actorProxy)
        {
            if (actorProxy is null)
            {
                throw new System.ArgumentNullException(nameof(actorProxy));
            }

            this.actorProxy = actorProxy;
        }

        public System.Threading.Tasks.Task<Test.TestValue> TestMethodAsync()
        {
            return this.actorProxy.InvokeMethodAsync<Test.TestValue>(""TestMethodAsync"");
        }
    }
}";

        await CreateTest(originalSource, "Test.TestActorClient.g.cs", generatedSource).RunAsync();
    }

    [Fact]
    public async Task TestMethodWithArgumentsAndReturnValue()
    {
        var originalSource = @"
using Dapr.Actors.Generators;
using System.Threading.Tasks;

namespace Test
{
    public record TestRequestValue(int Value);

    public record TestReturnValue(int Value);

    [GenerateActorClient]
    public interface ITestActor
    {
        Task<TestReturnValue> TestMethodAsync(TestRequestValue value);
    }
}";

        var generatedSource = @"// <auto-generated/>
#nullable enable
namespace Test
{
    public sealed class TestActorClient : Test.ITestActor
    {
        private readonly Dapr.Actors.Client.ActorProxy actorProxy;
        public TestActorClient(Dapr.Actors.Client.ActorProxy actorProxy)
        {
            if (actorProxy is null)
            {
                throw new System.ArgumentNullException(nameof(actorProxy));
            }

            this.actorProxy = actorProxy;
        }

        public System.Threading.Tasks.Task<Test.TestReturnValue> TestMethodAsync(Test.TestRequestValue value)
        {
            return this.actorProxy.InvokeMethodAsync<Test.TestRequestValue, Test.TestReturnValue>(""TestMethodAsync"", value);
        }
    }
}";

        await CreateTest(originalSource, "Test.TestActorClient.g.cs", generatedSource).RunAsync();
    }

    [Fact]
    public async Task TestMethodWithCancellationTokenArgument()
    {
        var originalSource = @"
using Dapr.Actors.Generators;
using System.Threading;
using System.Threading.Tasks;

namespace Test
{
    [GenerateActorClient]
    public interface ITestActor
    {
        Task TestMethodAsync(CancellationToken cancellationToken);
    }
}";

        var generatedSource = @"// <auto-generated/>
#nullable enable
namespace Test
{
    public sealed class TestActorClient : Test.ITestActor
    {
        private readonly Dapr.Actors.Client.ActorProxy actorProxy;
        public TestActorClient(Dapr.Actors.Client.ActorProxy actorProxy)
        {
            if (actorProxy is null)
            {
                throw new System.ArgumentNullException(nameof(actorProxy));
            }

            this.actorProxy = actorProxy;
        }

        public System.Threading.Tasks.Task TestMethodAsync(System.Threading.CancellationToken cancellationToken)
        {
            return this.actorProxy.InvokeMethodAsync(""TestMethodAsync"", cancellationToken);
        }
    }
}";

        await CreateTest(originalSource, "Test.TestActorClient.g.cs", generatedSource).RunAsync();
    }

    [Fact]
    public async Task TestMethodWithDefaultCancellationTokenArgument()
    {
        var originalSource = @"
using Dapr.Actors.Generators;
using System.Threading;
using System.Threading.Tasks;

namespace Test
{
    [GenerateActorClient]
    public interface ITestActor
    {
        Task TestMethodAsync(CancellationToken cancellationToken = default);
    }
}";

        var generatedSource = @"// <auto-generated/>
#nullable enable
namespace Test
{
    public sealed class TestActorClient : Test.ITestActor
    {
        private readonly Dapr.Actors.Client.ActorProxy actorProxy;
        public TestActorClient(Dapr.Actors.Client.ActorProxy actorProxy)
        {
            if (actorProxy is null)
            {
                throw new System.ArgumentNullException(nameof(actorProxy));
            }

            this.actorProxy = actorProxy;
        }

        public System.Threading.Tasks.Task TestMethodAsync(System.Threading.CancellationToken cancellationToken = default)
        {
            return this.actorProxy.InvokeMethodAsync(""TestMethodAsync"", cancellationToken);
        }
    }
}";

        await CreateTest(originalSource, "Test.TestActorClient.g.cs", generatedSource).RunAsync();
    }

    [Fact]
    public async Task TestMethodWithValueAndCancellationTokenArguments()
    {
        var originalSource = @"
using Dapr.Actors.Generators;
using System.Threading;
using System.Threading.Tasks;

namespace Test
{
    public record TestValue(int Value);

    [GenerateActorClient]
    public interface ITestActor
    {
        Task TestMethodAsync(TestValue value, CancellationToken cancellationToken);
    }
}
";

        var generatedSource = @"// <auto-generated/>
#nullable enable
namespace Test
{
    public sealed class TestActorClient : Test.ITestActor
    {
        private readonly Dapr.Actors.Client.ActorProxy actorProxy;
        public TestActorClient(Dapr.Actors.Client.ActorProxy actorProxy)
        {
            if (actorProxy is null)
            {
                throw new System.ArgumentNullException(nameof(actorProxy));
            }

            this.actorProxy = actorProxy;
        }

        public System.Threading.Tasks.Task TestMethodAsync(Test.TestValue value, System.Threading.CancellationToken cancellationToken)
        {
            return this.actorProxy.InvokeMethodAsync<Test.TestValue>(""TestMethodAsync"", value, cancellationToken);
        }
    }
}";

        await CreateTest(originalSource, "Test.TestActorClient.g.cs", generatedSource).RunAsync();
    }

    [Fact]
    public async Task TestMethodWithValueAndDefaultCancellationTokenArguments()
    {
        var originalSource = @"
using Dapr.Actors.Generators;
using System.Threading;
using System.Threading.Tasks;

namespace Test
{
    public record TestValue(int Value);

    [GenerateActorClient]
    public interface ITestActor
    {
        Task TestMethodAsync(TestValue value, CancellationToken cancellationToken = default);
    }
}
";

        var generatedSource = @"// <auto-generated/>
#nullable enable
namespace Test
{
    public sealed class TestActorClient : Test.ITestActor
    {
        private readonly Dapr.Actors.Client.ActorProxy actorProxy;
        public TestActorClient(Dapr.Actors.Client.ActorProxy actorProxy)
        {
            if (actorProxy is null)
            {
                throw new System.ArgumentNullException(nameof(actorProxy));
            }

            this.actorProxy = actorProxy;
        }

        public System.Threading.Tasks.Task TestMethodAsync(Test.TestValue value, System.Threading.CancellationToken cancellationToken = default)
        {
            return this.actorProxy.InvokeMethodAsync<Test.TestValue>(""TestMethodAsync"", value, cancellationToken);
        }
    }
}";

        await CreateTest(originalSource, "Test.TestActorClient.g.cs", generatedSource).RunAsync();
    }

    [Fact]
    public async Task TestMethodWithReversedArguments()
    {
        var originalSource = @"
using Dapr.Actors.Generators;
using System.Threading;
using System.Threading.Tasks;

namespace Test
{
    public record TestValue(int Value);

    [GenerateActorClient]
    public interface ITestActor
    {
        Task TestMethodAsync(CancellationToken cancellationToken, int value);
    }
}";

        var test = CreateTest(originalSource);

        test.TestState.ExpectedDiagnostics.Add(
            new DiagnosticResult("DAPR0001", DiagnosticSeverity.Error)
                .WithSpan(13, 48, 13, 65)
                .WithMessage("Cancellation tokens must be the last argument"));

        await test.RunAsync();
    }

    [Fact]
    public async Task TestMethodWithTooManyArguments()
    {
        var originalSource = @"
using Dapr.Actors.Generators;
using System.Threading;
using System.Threading.Tasks;

namespace Test
{
    public record TestValue(int Value);

    [GenerateActorClient]
    public interface ITestActor
    {
        Task TestMethodAsync(int value1, int value2);
    }
}";

        var test = CreateTest(originalSource);

        test.TestState.ExpectedDiagnostics.Add(
            new DiagnosticResult("DAPR0002", DiagnosticSeverity.Error)
                .WithSpan(13, 14, 13, 29)
                .WithMessage("Only methods with a single argument or a single argument followed by a cancellation token are supported"));

        await test.RunAsync();
    }

    [Fact]
    public async Task TestMethodWithFarTooManyArguments()
    {
        var originalSource = @"
using Dapr.Actors.Generators;
using System.Threading;
using System.Threading.Tasks;

namespace Test
{
    public record TestValue(int Value);

    [GenerateActorClient]
    public interface ITestActor
    {
        Task TestMethodAsync(int value1, int value2, CancellationToken cancellationToken);
    }
}";

        var test = CreateTest(originalSource);

        test.TestState.ExpectedDiagnostics.Add(
            new DiagnosticResult("DAPR0002", DiagnosticSeverity.Error)
                .WithSpan(13, 14, 13, 29)
                .WithMessage("Only methods with a single argument or a single argument followed by a cancellation token are supported"));

        await test.RunAsync();
    }
}
